﻿using System;

namespace Aeon.Emulator.Memory
{
    /// <summary>
    /// Represents a block of XMS memory.
    /// </summary>
    internal struct XmsBlock : IEquatable<XmsBlock>
    {
        #region Constructors
        /// <summary>
        /// Initializes a new XmsBlock struct.
        /// </summary>
        /// <param name="handle">Handle which owns the block.</param>
        /// <param name="offset">Offset of block from XMS base.</param>
        /// <param name="length">Length of block in bytes.</param>
        /// <param name="used">Value indicating whether the block is in use.</param>
        public XmsBlock(int handle, uint offset, uint length, bool used)
        {
            this.handle = handle;
            this.offset = offset;
            this.length = length;
            this.used = used;
        }
        #endregion

        #region Public Properties
        /// <summary>
        /// Gets the handle which owns the block.
        /// </summary>
        public int Handle
        {
            get { return this.handle; }
        }
        /// <summary>
        /// Gets the offset of the block from the XMS base address.
        /// </summary>
        public uint Offset
        {
            get { return this.offset; }
        }
        /// <summary>
        /// Gets the length of the block in bytes.
        /// </summary>
        public uint Length
        {
            get { return this.length; }
        }
        /// <summary>
        /// Gets a value indicating whether the block is in use.
        /// </summary>
        public bool IsUsed
        {
            get { return this.used; }
        }
        #endregion

        #region Public Methods
        /// <summary>
        /// Returns a string representation of the block.
        /// </summary>
        /// <returns>String representation of the block.</returns>
        public override string ToString()
        {
            if(this.used)
                return string.Format("{0:X4}: {1:X8} to {2:X8}", this.handle, this.offset, this.offset + this.length);
            else
                return "Free";
        }
        /// <summary>
        /// Returns a value indicating whether the block is equal to another object.
        /// </summary>
        /// <param name="obj">Other object to test for equality.</param>
        /// <returns>Value indicating whether objects are equal.</returns>
        public override bool Equals(object obj)
        {
            if(obj is XmsBlock)
                return Equals((XmsBlock)obj);
            else
                return false;
        }
        /// <summary>
        /// Returns a code suitable for hashing algorithms.
        /// </summary>
        /// <returns>Code suitable for hashing algorithms.</returns>
        public override int GetHashCode()
        {
            return this.handle ^ (int)this.offset ^ (int)this.length;
        }
        /// <summary>
        /// Returns a value indicating whether the block is equal to another block.
        /// </summary>
        /// <param name="other">Other block to test for equality.</param>
        /// <returns>Value indicating whether blocks are equal.</returns>
        public bool Equals(XmsBlock other)
        {
            return this.handle == other.handle && this.offset == other.offset && this.length == other.length && this.used == other.used;
        }
        /// <summary>
        /// Allocates a block of memory from a free block.
        /// </summary>
        /// <param name="handle">Handle making the allocation.</param>
        /// <param name="length">Length of the requested block in bytes.</param>
        /// <returns>Array of blocks to replace this block.</returns>
        public XmsBlock[] Allocate(int handle, uint length)
        {
            if(this.used)
                throw new InvalidOperationException();
            if(length > this.length)
                throw new ArgumentOutOfRangeException("length");

            if(length == this.length)
                return new XmsBlock[1] { new XmsBlock(handle, this.offset, length, true) };

            var blocks = new XmsBlock[2];

            blocks[0] = new XmsBlock(handle, this.offset, length, true);
            blocks[1] = new XmsBlock(0, this.offset + length, this.length - length, false);

            return blocks;
        }
        /// <summary>
        /// Frees a used block of memory.
        /// </summary>
        /// <returns>Freed block to replace this block.</returns>
        public XmsBlock Free()
        {
            return new XmsBlock(0, this.offset, this.length, false);
        }
        /// <summary>
        /// Merges two contiguous unused blocks of memory.
        /// </summary>
        /// <param name="other">Other unused block to merge with.</param>
        /// <returns>Merged block of memory.</returns>
        public XmsBlock Join(XmsBlock other)
        {
            if(this.used || other.used)
                throw new InvalidOperationException();
            if(this.offset + length != other.offset)
                throw new ArgumentException();

            return new XmsBlock(0, this.offset, this.length + other.length, false);
        }
        #endregion

        #region Private Fields
        /// <summary>
        /// Handle which owns the block.
        /// </summary>
        private int handle;
        /// <summary>
        /// Offset of the block from the XMS base address.
        /// </summary>
        private uint offset;
        /// <summary>
        /// Length of the block in bytes.
        /// </summary>
        private uint length;
        /// <summary>
        /// Indicates whether the block is in use.
        /// </summary>
        private bool used;
        #endregion
    }
}
